package com.jcake.services.client.impl;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

import com.jcake.services.client.Account;
import com.jcake.services.client.HttpProtocolException;
import com.jcake.services.client.Invocation;
import com.jcake.services.client.InvocationWrapper;
import com.jcake.services.client.RequestInvoker;
import com.jcake.services.client.ResultTransformationException;
import com.jcake.services.client.WsfClientException;
import com.jcake.services.client.InvocationWrapper.InvocationStatus;

public class HttpClientRequestInvoker implements RequestInvoker, InitializingBean {

	private Logger logger = LoggerFactory.getLogger(HttpClientRequestInvoker.class);

	private static final String HMAC_SHA1 = "HmacSHA1";

	public static final String PARAM_ACCESS_KEY = "AccessKey";
	public static final String PARAM_SIGNATURE = "Signature";
	public static final String PARAM_SIGNATURE_VERSION = "SignatureVersion";
	public static final String PARAM_TIMESTAMP = "Timestamp";
	public static final String PARAM_VERSION = "Version";
	public static final String PARAM_ACTION = "Action";

	private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 1000, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(),
			new ThreadPoolExecutor.CallerRunsPolicy());

	private HttpClient httpClient;

	public void afterPropertiesSet() throws Exception {
		if (httpClient == null) {
			MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
			connectionManager.getParams().setConnectionTimeout(4000);
			connectionManager.getParams().setDefaultMaxConnectionsPerHost(250);
			connectionManager.getParams().setMaxConnectionsPerHost(HostConfiguration.ANY_HOST_CONFIGURATION, 500);
			connectionManager.getParams().setMaxTotalConnections(2000);
			connectionManager.getParams().setSoTimeout(15000);
			httpClient = new HttpClient(connectionManager);
		}
	}

	public void setHttpClient(HttpClient httpClient) {
		this.httpClient = httpClient;
	}

	public void invokeRequests(List<InvocationWrapper> invocations) throws WsfClientException {
		final CountDownLatch countDownLatch = new CountDownLatch(invocations.size());
		for (final InvocationWrapper invocationWrapper : invocations) {
				threadPoolExecutor.execute(new Runnable() {
					public void run() {
						try {
							invokeRequest(invocationWrapper.getInvocation());
							invocationWrapper.setStatus(InvocationStatus.STATUS_OK);
						} catch (Exception e) {
							invocationWrapper.setStatus(InvocationStatus.STATUS_ERROR);
							invocationWrapper.setException(e);
						} finally {
							countDownLatch.countDown();
						}
						
					}
				});
		}
		try {
			countDownLatch.await(60, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			throw new WsfClientException("WSF-Client was not able to gather responses for more than 60 seconds.");
		}
	}

	public void invokeRequest(Invocation invocation) throws WsfClientException {
		String endpoint = invocation.getEndpoint();
		Account account = invocation.getAccount();
		String action = invocation.getAction();
		String version = invocation.getVersion();
		String signatureVersion = invocation.getSignatureVersion();

		Map<String, String> parameters = invocation.getParameters();
		Date requestDate = new Date();

		PostMethod method = new PostMethod(endpoint);
		method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

		List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
		nameValuePairs.add(new NameValuePair(PARAM_ACTION, action));
		nameValuePairs.add(new NameValuePair(PARAM_VERSION, version));
		if (parameters != null) {
			for (Entry<String, String> parameter : parameters.entrySet()) {
				nameValuePairs.add(new NameValuePair(parameter.getKey(), parameter.getValue()));
			}
		}
		if (account != null) {
			nameValuePairs.add(new NameValuePair(PARAM_TIMESTAMP, DateFormatUtils.ISO_DATETIME_FORMAT.format(requestDate)));
			nameValuePairs.add(new NameValuePair(PARAM_SIGNATURE_VERSION, signatureVersion));
			nameValuePairs.add(new NameValuePair(PARAM_ACCESS_KEY, account.getAccessKey()));
			String signature = calculateSignature(invocation, requestDate);

			nameValuePairs.add(new NameValuePair(PARAM_SIGNATURE, signature));
		}

		method.setRequestBody(nameValuePairs.toArray(new NameValuePair[nameValuePairs.size()]));

		logger.debug("Requesting URL {}?{}", endpoint, formatParameters(nameValuePairs));

		try {
			int status = httpClient.executeMethod(method);
			if (status == HttpStatus.SC_OK) {
				String response = IOUtils.toString(method.getResponseBodyAsStream(), "UTF-8");
				invocation.getResultTransformer().transform(response);
			} else {
				throw new HttpProtocolException(status, method.getResponseBodyAsString());
			}
		} catch (HttpException e) {
			throw new WsfClientException("HttpException caught.", e);
		} catch (IOException e) {
			throw new WsfClientException("An I/O error occured.", e);
		} catch (ResultTransformationException e) {
			throw new WsfClientException("Could not transform JSON response.", e);
		} finally {
			method.releaseConnection();
		}
	}

	private String formatParameters(List<NameValuePair> nameValuePairs) {
		StringBuilder buff = new StringBuilder();
		for (NameValuePair nameValuePair : nameValuePairs) {
			buff.append(nameValuePair.getName()).append("=").append(nameValuePair.getValue()).append("&");
		}
		return buff.substring(0, buff.length() - 1);
	}

	private String calculateSignature(Invocation invocation, Date requestDate) {
		List<String> sortedNames = new ArrayList<String>();
		Map<String, String> params = new HashMap<String, String>();
		params.putAll(invocation.getParameters());
		params.put(PARAM_VERSION, invocation.getVersion());
		params.put(PARAM_ACTION, invocation.getAction());

		if (invocation.getAccount() != null) {
			params.put(PARAM_SIGNATURE_VERSION, invocation.getSignatureVersion());
			params.put(PARAM_TIMESTAMP, DateFormatUtils.ISO_DATETIME_FORMAT.format(requestDate));
			params.put(PARAM_ACCESS_KEY, invocation.getAccount().getAccessKey());
		}

		for (Object name : params.keySet()) {
			sortedNames.add(name.toString());
		}

		Collections.sort(sortedNames, String.CASE_INSENSITIVE_ORDER);

		StringBuilder signatureData = new StringBuilder();
		StringBuilder signatureDataForLogger = new StringBuilder();
		for (String name : sortedNames) {
			signatureData.append(name);
			signatureDataForLogger.append(name).append("=");
			String value = params.get(name);
			signatureData.append(value);
			signatureDataForLogger.append(value).append("&");
		}

		try {
			SecretKeySpec secretKeySpec = new SecretKeySpec(invocation.getAccount().getSecretKey().getBytes(), HMAC_SHA1);
			Mac mac = Mac.getInstance(HMAC_SHA1);
			mac.init(secretKeySpec);

			logger.debug("Data for signature: {}", signatureData.toString());

			byte[] rawSignature = mac.doFinal(signatureData.toString().getBytes());
			String signature = new String(Base64.encodeBase64(rawSignature));

			logger.debug("Data for signature pretty: {}, Signature={}", signatureDataForLogger.toString(), signature);

			return signature;
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("Cannot get an instance of Mac " + HMAC_SHA1);
		} catch (InvalidKeyException e) {
			throw new RuntimeException("Invalid secret key.");
		}
	}

}
